package cc.shacocloud.mirage.utils.resource;

import cc.shacocloud.mirage.utils.AppUtil;
import cc.shacocloud.mirage.utils.ClassUtil;
import cc.shacocloud.mirage.utils.charSequence.CharUtil;
import cc.shacocloud.mirage.utils.charSequence.StrUtil;
import cc.shacocloud.mirage.utils.collection.CollUtil;
import cc.shacocloud.mirage.utils.file.FilesUtil;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

import static cc.shacocloud.mirage.utils.file.FilesUtil.OWNER_DIRECTORY_PERMISSIONS;
import static cc.shacocloud.mirage.utils.file.FilesUtil.getFileAttributes;

/**
 * 用于将资源位置解析为文件系统中的文件的实用工具方法
 */
public abstract class ResourceUtil {
    
    /**
     * 用于从类路径加载的伪 URL 前缀： "classpath:".
     */
    public static final String CLASSPATH_URL_PREFIX = "classpath:";
    
    /**
     * 用于从文件系统加载的 URL 前缀： "file:".
     */
    public static final String FILE_URL_PREFIX = "file:";
    
    /**
     * 用于从 jar 文件加载的 URL 前缀： "jar:".
     */
    public static final String JAR_URL_PREFIX = "jar:";
    
    /**
     * 文件系统中文件的 URL 协议："file".
     */
    public static final String URL_PROTOCOL_FILE = "file";
    
    /**
     * jar 文件中条目的 URL 协议： "jar".
     */
    public static final String URL_PROTOCOL_JAR = "jar";
    
    /**
     * 来自 zip 文件的条目的 URL 协议："zip".
     */
    public static final String URL_PROTOCOL_ZIP = "zip";
    
    /**
     * 常规 jar 文件的文件扩展名： ".jar".
     */
    public static final String JAR_FILE_EXTENSION = ".jar";
    
    /**
     * JAR URL 和 JAR 中的文件路径之间的分隔符： "!/".
     */
    public static final String JAR_URL_SEPARATOR = "!/";
    
    /**
     * 类路径资源缓存目录，仅仅在 jar 模式下生效，非 jar模式为空
     */
    @Nullable
    public static final Path classPathCacheDirectory;
    
    static {
        try {
            Path path = null;
            if (AppUtil.isJarRun()) {
                Path tempDirectory = Paths.get(System.getProperty("java.io.tmpdir"));
                File jarFile = AppUtil.findSource(AppUtil.getStartClass());
                String appFileName = Objects.isNull(jarFile) ? ""
                        : jarFile.toPath().getFileName().toString().replace(".jar", "") + "-";
                path = tempDirectory.resolve(appFileName + "mirage-classpath-cache-" + UUID.randomUUID());
                Files.createDirectories(path, getFileAttributes(path.getFileSystem(), OWNER_DIRECTORY_PERMISSIONS));
                path.toFile().deleteOnExit();
            }
            classPathCacheDirectory = path;
        } catch (IOException e) {
            throw new RuntimeException("创建类路径缓存目录发生例外！", e);
        }
    }
    
    /**
     * 获取资源对象
     * <p>
     * 支持 {@link #CLASSPATH_URL_PREFIX} 和 {@link #FILE_URL_PREFIX} 两种协议，如果都不是则默认是  {@link #FILE_URL_PREFIX}
     *
     * @param resourceLocation 资源路径
     * @return {@link Resource}
     */
    @NotNull
    @Contract("_ -> new")
    public static Resource getResource(@NotNull String resourceLocation) {
        // 类路径，如果是 jar 运行方式的类路径资源复制到临时解压目录下保存
        if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) {
            String resourcePath = resourceLocation.substring(CLASSPATH_URL_PREFIX.length());
            Resource resource = new ClassPathResource(resourcePath);
            
            if (AppUtil.isJarRun() && resource.exists()) {
                try {
                    Path path = Objects.requireNonNull(classPathCacheDirectory).resolve(resource.getPath());
                    
                    if (!Files.exists(path)) {
                        FilesUtil.writeInputStream(path, resource.getStream());
                        path.toFile().deleteOnExit();
                    }
                    
                    resource = new FileSystemResource(path);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            
            return resource;
        }
        // 文件系统
        else if (resourceLocation.startsWith(FILE_URL_PREFIX)) {
            String resourcePath = resourceLocation.substring(FILE_URL_PREFIX.length());
            return new FileSystemResource(resourcePath);
        } else {
            
            if (resourceLocation.contains(":")) {
                String protocol = StrUtil.subPre(resourceLocation, resourceLocation.indexOf(":") + 1);
                throw new IllegalArgumentException(String.format("不支持的资源协议类型：%s", protocol));
            }
            
            // 默认使用文件系统
            return new FileSystemResource(resourceLocation);
        }
    }
    
    /**
     * 返回给定的资源位置是否为 URL
     *
     * @see #CLASSPATH_URL_PREFIX
     * @see java.net.URL
     */
    public static boolean isUrl(@Nullable String resourceLocation) {
        if (resourceLocation == null) {
            return false;
        }
        if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) {
            return true;
        }
        try {
            new URL(resourceLocation);
            return true;
        } catch (MalformedURLException ex) {
            return false;
        }
    }
    
    /**
     * 将给定的资源位置解析为 {@code java.net.URL}
     *
     * @param resourceLocation 要解析的资源位置
     * @return {@link URL}
     * @throws FileNotFoundException 如果无法将资源解析为 URL
     */
    @NotNull
    public static URL getURL(@NotNull String resourceLocation) throws FileNotFoundException {
        if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) {
            String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length());
            ClassLoader cl = ClassUtil.getDefaultClassLoader();
            URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path));
            if (url == null) {
                throw new FileNotFoundException(String.format("类路径资源 %s 无法解析为 URL，因为它不存在", path));
            }
            return url;
        }
        
        try {
            return new URL(resourceLocation);
        } catch (MalformedURLException ex) {
            // 尝试文件
            try {
                return new File(resourceLocation).toURI().toURL();
            } catch (MalformedURLException ex2) {
                throw new FileNotFoundException(String.format("资源位置 %s 既不是 URL，也不是格式正确的文件路径", resourceLocation));
            }
        }
    }
    
    /**
     * 将给定的资源位置解析为 {@code java.io.File}，即文件系统中的文件
     * <p>
     * 不检查文件是否实际存在，只需返回给定位置对应的文件
     *
     * @param resourceLocation 要解析的资源位置
     * @return 相应的文件对象
     * @throws FileNotFoundException 如果无法将资源解析为文件系统中的文件
     */
    @NotNull
    public static File getFile(@NotNull String resourceLocation) throws FileNotFoundException {
        if (resourceLocation.startsWith(CLASSPATH_URL_PREFIX)) {
            String path = resourceLocation.substring(CLASSPATH_URL_PREFIX.length());
            String description = "类路径资源 [" + path + "]";
            ClassLoader cl = ClassUtil.getDefaultClassLoader();
            URL url = (cl != null ? cl.getResource(path) : ClassLoader.getSystemResource(path));
            if (url == null) {
                throw new FileNotFoundException(String.format("类路径资源 %s 无法解析为绝对路径文件，因为它不存在", path));
            }
            return getFile(url, description);
        }
        try {
            return getFile(new URL(resourceLocation));
        } catch (MalformedURLException ex) {
            // 尝试文件
            return new File(resourceLocation);
        }
    }
    
    /**
     * 将给定的资源 URL 解析为 {@code java.io.File}，即文件系统中的文件
     *
     * @param resourceUrl 要解析的资源 URL
     * @return 相应的文件对象
     * @throws FileNotFoundException 如果无法将 URL 解析为文件系统中的文件
     */
    @Contract("_ -> new")
    public static @NotNull File getFile(URL resourceUrl) throws FileNotFoundException {
        return getFile(resourceUrl, "URL");
    }
    
    /**
     * 将给定的资源 URL 解析为 {@code java.io.File}，即文件系统中的文件.
     *
     * @param resourceUrl 要解析的资源 URL
     * @param description 为其创建 URL 的原始资源的说明（例如，类路径位置）
     * @return 相应的文件对象
     * @throws FileNotFoundException 如果无法将 URL 解析为文件系统中的文件
     */
    @NotNull
    @Contract("_, _ -> new")
    public static File getFile(@NotNull URL resourceUrl, String description) throws FileNotFoundException {
        if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) {
            throw new FileNotFoundException(String.format("%s 无法解析为绝对文件路径，因为它不是本地文件系统协议：%s",
                    description, resourceUrl));
        }
        try {
            return new File(toURI(resourceUrl).getSchemeSpecificPart());
        } catch (URISyntaxException ex) {
            return new File(resourceUrl.getFile());
        }
    }
    
    /**
     * 确定给定的 URL 是否指向文件系统中的资源
     *
     * @see #URL_PROTOCOL_FILE
     */
    public static boolean isFileURL(@NotNull URL url) {
        String protocol = url.getProtocol();
        return URL_PROTOCOL_FILE.equals(protocol);
    }
    
    /**
     * 确定给定的 URL 是否指向 jar 文件中的资源
     *
     * @see #URL_PROTOCOL_JAR
     * @see #URL_PROTOCOL_ZIP
     */
    public static boolean isJarURL(@NotNull URL url) {
        String protocol = url.getProtocol();
        return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_ZIP.equals(protocol));
    }
    
    /**
     * 确定给定的 URL 是否指向 jar 文件本身，即具有协议 file 并以 .jar 扩展名结尾
     */
    public static boolean isJarFileURL(@NotNull URL url) {
        return (URL_PROTOCOL_FILE.equals(url.getProtocol()) && url.getPath().toLowerCase().endsWith(JAR_FILE_EXTENSION));
    }
    
    /**
     * 从给定的 URL（可能指向 jar 文件中的资源或 jar 文件本身）中提取实际 jar 文件的 URL
     *
     * @param jarUrl jarUrl
     * @return 实际 jar 文件的 URL
     * @throws MalformedURLException 如果无法提取有效的 jar 文件 URL
     */
    @NotNull
    public static URL extractJarFileURL(@NotNull URL jarUrl) throws MalformedURLException {
        String urlFile = jarUrl.getFile();
        int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR);
        if (separatorIndex != -1) {
            String jarFile = urlFile.substring(0, separatorIndex);
            try {
                return new URL(jarFile);
            } catch (MalformedURLException ex) {
                // 原始 jar URL 中可能没有协议，例如 "jar:C:/mypath/myjar.jar".
                // 这通常表示 jar 文件驻留在文件系统中
                if (!jarFile.startsWith("/")) {
                    jarFile = "/" + jarFile;
                }
                return new URL(FILE_URL_PREFIX + jarFile);
            }
        } else {
            return jarUrl;
        }
    }
    
    
    /**
     * 为给定 URL 创建一个 URI 实例，首先将空格替换为 %20 URI 编码
     *
     * @param url 要转换为 URI 实例的 URL
     * @return URI 实例
     * @throws URISyntaxException 如果网址不是有效的 URI
     * @see java.net.URL#toURI()
     */
    @NotNull
    @Contract("_ -> new")
    public static URI toURI(@NotNull URL url) throws URISyntaxException {
        return toURI(url.toString());
    }
    
    /**
     * 为给定位置字符串创建一个 URI 实例，首先将空格替换为 %20 URI 编码
     *
     * @param location 要转换为 URI 实例的位置字符串
     * @return URI 实例
     * @throws URISyntaxException 如果网址不是有效的 URI
     */
    @Contract("_ -> new")
    public static @NotNull URI toURI(@NotNull String location) throws URISyntaxException {
        return new URI(location.replace(" ", "%20"));
    }
    
    /**
     * 修复路径 <br>
     * 如果原路径尾部有分隔符，则保留为标准分隔符（/），否则不保留
     * <ol>
     * <li>1. 统一用 /</li>
     * <li>2. 多个 / 转换为一个 /</li>
     * <li>3. 去除左边空格</li>
     * <li>4. .. 和 . 转换为绝对路径，当..多于已有路径时，直接返回根路径</li>
     * </ol>
     * <p>
     * 栗子：
     *
     * <pre>
     * "/foo//" =》 "/foo/"
     * "/foo/./" =》 "/foo/"
     * "/foo/../bar" =》 "/bar"
     * "/foo/../bar/" =》 "/bar/"
     * "/foo/../bar/../baz" =》 "/baz"
     * "/../" =》 "/"
     * "foo/bar/.." =》 "foo"
     * "foo/../bar" =》 "bar"
     * "foo/../../bar" =》 "bar"
     * "//server/foo/../bar" =》 "/server/bar"
     * "//server/../bar" =》 "/bar"
     * "C:\\foo\\..\\bar" =》 "C:/bar"
     * "C:\\..\\bar" =》 "C:/bar"
     * "~/foo/../bar/" =》 "~/bar/"
     * "~/../bar" =》 普通用户运行是'bar的home目录'，ROOT用户运行是'/bar'
     * </pre>
     *
     * @param path 原路径
     * @return 修复后的路径
     */
    public static String normalize(String path) {
        if (StrUtil.isEmpty(path)) {
            return path;
        }
        
        // 兼容Spring风格的ClassPath路径，去除前缀，不区分大小写
        String pathToUse = StrUtil.removePrefixIgnoreCase(path, CLASSPATH_URL_PREFIX);
        // 去除file:前缀
        pathToUse = StrUtil.removePrefixIgnoreCase(pathToUse, FILE_URL_PREFIX);
        
        // 识别home目录形式，并转换为绝对路径
        if (StrUtil.startWith(pathToUse, '~')) {
            pathToUse = System.getProperty("user.home") + pathToUse.substring(1);
        }
        
        // 统一使用斜杠
        pathToUse = pathToUse.replaceAll("[/\\\\]+", StrUtil.SLASH);
        // 去除开头空白符，末尾空白符合法，不去除
        pathToUse = StrUtil.trimStart(pathToUse);
        //兼容Windows下的共享目录路径（原始路径如果以\\开头，则保留这种路径）
        if (path.startsWith("\\\\")) {
            pathToUse = "\\" + pathToUse;
        }
        
        String prefix = StrUtil.EMPTY;
        int prefixIndex = pathToUse.indexOf(StrUtil.COLON);
        if (prefixIndex > -1) {
            // 可能Windows风格路径
            prefix = pathToUse.substring(0, prefixIndex + 1);
            if (StrUtil.startWith(prefix, CharUtil.SLASH)) {
                // 去除类似于/C:这类路径开头的斜杠
                prefix = prefix.substring(1);
            }
            if (!prefix.contains(StrUtil.SLASH)) {
                pathToUse = pathToUse.substring(prefixIndex + 1);
            } else {
                // 如果前缀中包含/,说明非Windows风格path
                prefix = StrUtil.EMPTY;
            }
        }
        if (pathToUse.startsWith(StrUtil.SLASH)) {
            prefix += StrUtil.SLASH;
            pathToUse = pathToUse.substring(1);
        }
        
        List<String> pathList = StrUtil.split(pathToUse, CharUtil.SLASH);
        
        List<String> pathElements = new LinkedList<>();
        int tops = 0;
        String element;
        for (int i = pathList.size() - 1; i >= 0; i--) {
            element = pathList.get(i);
            // 只处理非.的目录，即只处理非当前目录
            if (!StrUtil.DOT.equals(element)) {
                if (StrUtil.DOUBLE_DOT.equals(element)) {
                    tops++;
                } else {
                    if (tops > 0) {
                        // 有上级目录标记时按照个数依次跳过
                        tops--;
                    } else {
                        // Normal path element found.
                        pathElements.add(0, element);
                    }
                }
            }
        }
        
        // issue#1703@Github
        if (tops > 0 && StrUtil.isEmpty(prefix)) {
            // 只有相对路径补充开头的..，绝对路径直接忽略之
            while (tops-- > 0) {
                //遍历完节点发现还有上级标注（即开头有一个或多个..），补充之
                // Normal path element found.
                pathElements.add(0, StrUtil.DOUBLE_DOT);
            }
        }
        
        return prefix + CollUtil.join(pathElements, StrUtil.SLASH);
    }
    
}
